﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading;
using Ensynch.Collections.Generic;
using Microsoft.ResourceManagement.Client;
using Microsoft.ResourceManagement.ObjectModel;

namespace Ensynch.FIM
{
    public delegate void AsyncResolve(ref DataRow row, RmAttributeValue rmAttribute, WindowsIdentity impersonatedUser);

    public class FimQueryAttribute
    {
        // Create a logger for use in this class.
        protected static readonly log4net.ILog log = log4net.LogManager.GetLogger(
                System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

        private FimQuery query;

        #region Constructors

        public FimQueryAttribute(FimQuery query, string attribute)
        {
            this.query = query;
            this.attributeRaw = attribute;
        }

        #endregion

        #region Public properties

        private bool includeRaw = false;
        public bool IncludeRaw
        {
            get
            {
                return includeRaw;
            }
            set
            {
                if (includeRaw != value)
                {
                    includeRaw = value;
                    onHasChanged();
                }
            }
        }

        private string aliasRaw;
        public string AliasRaw
        {
            get
            {
                return aliasRaw;
            }
            set
            {
                if (aliasRaw != value)
                {
                    aliasRaw = value;
                    // Reset the calculated value.
                    aliasTrimmed = null;
                    onHasChanged();
                }
            }
        }

        private string attributeRaw;
        public string AttributeRaw
        {
            get
            {
                return attributeRaw;
            }
        }

        private string attributeTrimmed;
        public string AttributeTrimmed
        {
            get
            {
                if (attributeTrimmed == null)
                {
                    attributeTrimmed = FimQueryAttribute.Trim(this.AttributeRaw);
                }
                return attributeTrimmed;
            }
        }

        private string aliasTrimmed;
        public string AliasTrimmed
        {
            get
            {
                if (aliasTrimmed == null)
                {
                    if (!string.IsNullOrEmpty(AliasRaw))
                    {
                        aliasTrimmed = FimQueryAttribute.Trim(this.AliasRaw);
                    }
                    else
                    {
                        aliasTrimmed = AttributeTrimmed;
                    }
                }
                return aliasTrimmed;
            }
        }

        public IEnumerable<FimQueryAttribute> SubAttributes
        {
            get
            {
                if (_subQuery == null)
                {
                    return null;
                }
                return _subQuery.FimQueryAttributes;
            }
        }

        public List<DataColumn> DataColumns
        {
            get
            {
                List<DataColumn> dataColumns = new List<DataColumn>();
                if (this.IncludeRaw)
                {
                    dataColumns.Add(new DataColumn(this.AliasTrimmed, this.Type));
                }
                if (SubAttributes != null)
                {
                    foreach (var subAttribute in SubAttributes)
                    {
                        dataColumns.AddRange(subAttribute.DataColumns.ToArray());
                    }
                }
                return dataColumns;
            }
        }

        private RmFactory.RmAttributeType? rmAttributeType;
        public RmFactory.RmAttributeType RmAttributeType
        {
            get
            {
                // RmFactory.RmAttributeType is a struct, so it will never be null if you
                // don't wrap it in Nullable.
                if (rmAttributeType == null)
                {
                    if (!AttributeTypeDictionary.ContainsKey(AttributeTrimmed))
                    {
                        rmAttributeType = RmFactory.RmAttributeType.String;
                        //log.Debug("[get_RmAttributeType] Not in dictionary: " + Attribute + " (assuming String)");
                    }
                    else
                    {
                        rmAttributeType = AttributeTypeDictionary[AttributeTrimmed];
                        //log.Debug("[get_RmAttributeType] Found type: " + Attribute + " = " + rmAttributeType);
                    }
                }
                return rmAttributeType.Value;
            }
        }

        private Type type;
        public Type Type
        {
            get
            {
                if (type == null)
                {
                    switch (RmAttributeType)
                    {
                        case RmFactory.RmAttributeType.Binary:
                            type = typeof(byte[]);
                            break;
                        case RmFactory.RmAttributeType.Boolean:
                            type = typeof(Boolean);
                            break;
                        case RmFactory.RmAttributeType.DateTime:
                            type = typeof(DateTime);
                            break;
                        case RmFactory.RmAttributeType.Integer:
                            type = typeof(int);
                            break;
                        case RmFactory.RmAttributeType.Reference:
                        case RmFactory.RmAttributeType.String:
                        default:
                            type = typeof(String);
                            break;
                    }
                }
                return type;
            }
        }

        public bool IsEmpty
        {
            get
            {
                if (IncludeRaw)
                {
                    return false;
                }
                if (_subQuery != null)
                {
                    foreach (var attribute in _subQuery.FimQueryAttributes)
                    {
                        if (!attribute.IsEmpty)
                        {
                            return false;
                        }
                    }
                }
                return true;
            }
        }

        #endregion

        #region Private properties

        private ObjectResolverQuery _subQuery;
        private ObjectResolverQuery subQuery
        {
            get
            {
                if (_subQuery == null)
                {
                    _subQuery = new ObjectResolverQuery(query.XPath + "/" + AttributeTrimmed, query.Tools);
                }
                return _subQuery;
            }
        }

        private SerializableDictionary<string, RmFactory.RmAttributeType> attributeTypeDictionary;
        private SerializableDictionary<string, RmFactory.RmAttributeType> AttributeTypeDictionary
        {
            get
            {
                if (attributeTypeDictionary == null)
                {
                    attributeTypeDictionary = query.Tools.GetAttributeTypeDictionary(query.ObjectType);
                }
                //log.Debug("[get_AttributeTypeDictionary] " + query.ObjectType);
                return attributeTypeDictionary;
            }
        }

        #endregion

        #region Public methods

        public void SetParameters(Dictionary<string, object> parameters)
        {
            if (_subQuery == null)
            {
                return;
            }
            _subQuery.Parameters = parameters;
        }

        public static string Trim(string input)
        {
            if (input == null)
            {
                return null;
            }
            if (input == string.Empty)
            {
                return input;
            }
            return input.TrimStart('[').TrimEnd(']');
        }

        public FimQueryAttribute GetSubAttribute(string attribute)
        {
            if (_subQuery == null)
            {
                return null;
            }
            return _subQuery.GetAttribute(attribute);
        }

        public void AddSubAttribute(string attribute, string alias)
        {
            subQuery.AddAttribute(attribute, alias);
            onHasChanged();
        }

        public void RemoveSubAttribute(string attribute)
        {
            subQuery.RemoveAttribute(attribute, false);
            onHasChanged();
        }

        public IAsyncResult ResolveAttributeValue(RmResource obj, ref DataRow row)
        {
            maybeResolveAttributeValue(ref obj, ref row);
            return maybeResolveSubAttributeValues(ref obj, ref row);
        }

        public override string ToString()
        {
            StringBuilder buffer = new StringBuilder();
            bool first = true;
            if (IncludeRaw)
            {
                if (string.IsNullOrEmpty(AliasRaw))
                {
                    buffer.Append(this.AttributeRaw);
                }
                else
                {
                    buffer.AppendFormat("{0} AS {1}", this.AttributeRaw, this.AliasRaw);
                }
                first = false;
            }
            if (_subQuery != null)
            {
                string subAttributeString = _subQuery.AttributeString;
                if (!string.IsNullOrEmpty(subAttributeString))
                {
                    if (first)
                    {
                        first = false;
                    }
                    else
                    {
                        buffer.Append("\r\n,");
                    }
                    buffer.Append(this.AttributeRaw + "." + subAttributeString.Replace("\r\n,", "\r\n," + this.AttributeRaw + "."));
                }
            }
            return buffer.ToString();
        }

        #endregion

        #region Private methods

        private void maybeResolveAttributeValue(ref RmResource obj, ref DataRow row)
        {
            if (!IncludeRaw)
            {
                return;
            }

            if (!row.Table.Columns.Contains(AliasTrimmed))
            {
                throw new IndexOutOfRangeException("[maybeResolveAttributeValue] No such column: " + AliasTrimmed);
            }

            if (!obj.ContainsKey(new RmAttributeName(AttributeTrimmed)) || obj[AttributeTrimmed] == null)
            {
                row[AliasTrimmed] = System.DBNull.Value;
                return;
            }

            RmAttributeValue rmAttribute = obj[AttributeTrimmed];
            RmFactory.RmAttributeType rmAttributeType;
            if (AttributeTypeDictionary.ContainsKey(AttributeTrimmed))
            {
                rmAttributeType = AttributeTypeDictionary[AttributeTrimmed];
            }
            else
            {
                rmAttributeType = RmFactory.RmAttributeType.String;
            }

            if (rmAttribute.IsMultiValue)
            {
                StringBuilder buffer = new StringBuilder();
                bool first = true;
                foreach (var value in rmAttribute.Values)
                {
                    if (rmAttributeType == RmFactory.RmAttributeType.Reference)
                    {
                        if (!first)
                        {
                            buffer.Append("; ");
                        }

                        buffer.Append(value.ToString());
                    }
                    else
                    {
                        buffer.AppendLine(value.ToString());
                    }

                    first = false;
                }
                row[AliasTrimmed] = buffer.ToString();
            }
            else if (rmAttribute.Value == null)
            {
                row[AliasTrimmed] = System.DBNull.Value;
            }
            //else if (attribute.Equals(FimTools.ATTRIBUTE_OBJECT_ID))
            //{
            //    return tools.PrintReference(
            //        obj[attribute].Value.ToString(),
            //        FimTools.FORMAT_GUID);
            //}
            //else if (attrTypeDict[attribute].Equals(FimTools.TYPE_REFERENCE))
            //{
            //    return tools.PrintReference(
            //        obj[attribute].Value.ToString(),
            //        format);
            //}
            else
            {
                switch (rmAttributeType)
                {
                    case RmFactory.RmAttributeType.Binary:
                        row[AliasTrimmed] = ((RmBinary)rmAttribute.Value).ValueAsBinary;
                        break;
                    default:
                        row[AliasTrimmed] = rmAttribute.Value;
                        break;
                }
            }
        }

        private IAsyncResult maybeResolveSubAttributeValues(ref RmResource obj, ref DataRow row)
        {
            if (_subQuery == null || !_subQuery.HasAttributes)
            {
                return null;
            }

            if (!obj.ContainsKey(new RmAttributeName(AttributeTrimmed)) || obj[AttributeTrimmed] == null)
            {
                return null;
            }

            if (RmAttributeType != RmFactory.RmAttributeType.Reference)
            {
                return null;
            }

            RmAttributeValue rmAttribute = obj[AttributeTrimmed];
            AsyncResolve asyncResolver = new AsyncResolve(resolveSubAttributeValues);
            return asyncResolver.BeginInvoke(ref row, rmAttribute, query.ImpersonatedUser, null, null);
        }

        private void resolveSubAttributeValues(ref DataRow row, RmAttributeValue rmAttribute, WindowsIdentity impersonatedUser)
        {
            WindowsImpersonationContext impersonationContext = null;
            if (impersonatedUser != null)
            {
                log.Debug("[resolveSubAttributeValues] Impersonating user: " + impersonatedUser.Name);
                impersonationContext = impersonatedUser.Impersonate();
                FimTools.LogCurrentUser();
            }

            if (rmAttribute.IsMultiValue)
            {
                StringBuilder buffer = new StringBuilder();
                int batchSize = 0;
                _subQuery.ClearObjects();
                for (int i = 0; i < rmAttribute.Values.Count; i++)
                {
                    var value = rmAttribute.Values[i];
                    _subQuery.AddObject(value.ToString());

                    if (++batchSize == ObjectResolverQuery.OBJECT_RESOLVER_BATCH_SIZE ||
                        i == rmAttribute.Values.Count - 1)
                    {
                        runSubQuery(ref row);

                        // Don't forget to reset the batch size.
                        batchSize = 0;
                        // And clear out the existing objects.
                        _subQuery.ClearObjects();
                    }
                }
            }
            else if (rmAttribute.Value != null)
            {
                _subQuery.ClearObjects();
                _subQuery.AddObject(rmAttribute.Value.ToString());
                runSubQuery(ref row);
            }

            if (impersonationContext != null)
            {
                impersonationContext.Undo();
                impersonationContext.Dispose();
                impersonationContext = null;
            }
        }

        private void runSubQuery(ref DataRow row)
        {
            FimQueryResult result = _subQuery.GetResult();
            result.ReadAll();
            StringBuilder buffer;

            foreach (var attribute in _subQuery.FimQueryAttributes)
            {
                if (attribute.IsEmpty)
                {
                    continue;
                }

                if (row[attribute.AliasTrimmed] == null || row[attribute.AliasTrimmed] == DBNull.Value)
                {
                    buffer = new StringBuilder();
                }
                else
                {
                    buffer = new StringBuilder(row[attribute.AliasTrimmed].ToString());
                }

                RmFactory.RmAttributeType rmAttributeType = result.AttributeTypeDictionary[attribute.AttributeTrimmed];

                foreach (DataRow subRow in result.DataTable.Rows)
                {
                    if (subRow[attribute.AliasTrimmed] == null || subRow[attribute.AliasTrimmed] == DBNull.Value ||
                        subRow[attribute.AliasTrimmed].ToString() == string.Empty)
                    {
                        continue;
                    }
                    if (buffer.Length > 0)
                    {
                        buffer.Append("; ");
                    }
                    buffer.Append(subRow[attribute.AliasTrimmed].ToString());
                }
                row[attribute.AliasTrimmed] = buffer.ToString();
            }
        }

        #endregion

        #region Events

        public event EventHandler HasChanged;

        private void onHasChanged()
        {
            if (HasChanged != null)
            {
                HasChanged(this, new EventArgs());
            }
        }

        #endregion

    }
}
